﻿using System;
#if SERVER
using System.Diagnostics.Contracts;
using System.Net.Sockets;
using System.Threading;
#endif
using System.IO;
using System.Runtime.Serialization;
using System.Text;
using System.Xml;

namespace Ping.Core
{
	/// <summary>
	/// The container that stores a single data object that can be any of the known types (mostly Result and Request).
	/// It implements methods to serialize and deserialize the object to/from a stream.
	/// </summary>
	public sealed class Packet
	{
		/// <summary>
		/// The maximum allowed size of a serialized packet.
		/// </summary>
		public const int MaxSize = 8*1024;

#if SERVER
		/// <summary>
		/// Reads a Data-packet from the given stream.
		/// </summary>
		/// <param name="reader"></param>
		/// <returns></returns>
		public static Data Read(BinaryReader reader)
		{
			Int32 length = reader.ReadInt32();
			return Deserialize(reader.BaseStream, length);
		}

		/// <summary>
		/// Writes the given data as a packet to the stream.
		/// </summary>
		/// <param name="writer"></param>
		/// <param name="data"></param>
		public static void Write(BinaryWriter writer, Data data)
		{
			var buffer = Serialize(data);
			writer.Write(buffer.Length);
			writer.Write(buffer);
		}

		/// <summary>
		/// Async state for reading requests.
		/// </summary>
		private class State
			: IAsyncResult
		{
			public State(NetworkStream stream, AsyncCallback handler, byte[] buffer)
			{
				Stream = stream;
				Handler = handler;
				Buffer = buffer;
			}

			public readonly NetworkStream Stream;
			public readonly AsyncCallback Handler;
			public readonly byte[] Buffer;

			public Data Data;
			public Exception Exception;

			public bool IsCompleted
			{
				get { return Data != null || Exception != null; }
			}

			public WaitHandle AsyncWaitHandle
			{
				get { throw new NotImplementedException(); }
			}

			public object AsyncState
			{
				get { return null; }
			}

			public bool CompletedSynchronously
			{
				get { return false; }
			}
		}

		/// <summary>
		/// Begins an asynchronous read-request to read the next data-packet
		/// </summary>
		/// <param name="stream"></param>
		/// <param name="handler"></param>
		/// <returns></returns>
		public static IAsyncResult BeginRead(NetworkStream stream, AsyncCallback handler)
		{
			var buffer = new byte[4];
			var result = new State(stream, handler, buffer);

			stream.BeginRead(buffer, 0, buffer.Length, EndReadPrivate, new State(stream, handler, buffer));

			return result;
		}

		private static void EndReadPrivate(IAsyncResult result)
		{
			var state = (State) result.AsyncState;

			try
			{
				// This handler is called even when the connection gets disconnected in between BeginRead / EndRead calls:
				// This must be properly!
				int bytesRead = state.Stream.EndRead(result);

				// Not being able to read those 4 bytes means that there's a "serios" problem with the connection:
				// An IOException reflects this problem (because it's not just an invalid request: data is missing).
				if (bytesRead != state.Buffer.Length)
					throw new IOException("Unable to read request length from connection");

				var reader = new BinaryReader(new MemoryStream(state.Buffer));
				int length = reader.ReadInt32();
				if (length > MaxSize)
					throw new ConnectionAbortException(string.Format("For performance and security reasons, packets with a size bigger than {0} KiB are not supported", MaxSize));
				if (length <= 0)
					throw new ConnectionAbortException("Invalid length specified");

				state.Data = Deserialize(state.Stream, length);
			}
			catch(Exception e)
			{
				state.Exception = e;
			}
			finally
			{
				state.Handler(state);
			}
		}

		public static Data EndRead(IAsyncResult result)
		{
			var state = (State) result;
			if (state.Exception != null)
				throw state.Exception;

			return state.Data;
		}
#endif

		/// <summary>
		/// The list of known types for the data contract serializer
		/// </summary>
		public static readonly Type[] KnownTypes = {
		                                           	typeof (Data),
		                                           	typeof (Request),
		                                           	typeof (Result),
		                                           	typeof (OperationResult),
		                                           	typeof (DnsRequest),
		                                           	typeof (DnsResult),
		                                           	typeof (PingRequest),
		                                           	typeof (PingResult),
		                                           	typeof (TracerouteRequest),
		                                           	typeof (TracerouteResult),
		                                           	typeof (GeoRequest),
		                                           	typeof (GeoResult),
													typeof (StatusRequest),
													typeof (StatusResult)
		                                           };

		public Data Data;

		public Packet()
		{}

		public Packet(Data inner)
		{
			Data = inner;
		}

		/// <summary>
		/// Serializes the given result into an xml document and serializes that one
		/// into a bytestream using the given encoding.
		/// </summary>
		/// <param name="data"></param>
		/// <returns></returns>
		public static byte[] Serialize(Data data)
		{
			if (data == null)
				throw new ArgumentNullException();

			var tmpStream = new MemoryStream();
			var settings = new XmlWriterSettings { Encoding = Encoding.UTF8 };
			using (XmlWriter writer = XmlWriter.Create(tmpStream, settings))
			{
				var serialzer = new DataContractSerializer(typeof(Packet), KnownTypes);
				serialzer.WriteObject(writer, new Packet { Data = data });
			}

			return tmpStream.ToArray();
		}

		/// <summary>
		/// Deserializes the given stream into a result object, if possible.
		/// </summary>
		/// <param name="stream">The stream to read from</param>
		/// <param name="length">The length of the paket</param>
		/// <returns>The deserialized paket</returns>
		/// <exception cref="ReadRequestException">In case the request was malformed</exception>
		/// <exception cref="Exception">In any other case (or a subclass thereof)</exception>
		public static Data Deserialize(Stream stream, int length)
		{
			if (stream == null)
				throw new ArgumentNullException();
			if (length <= 0)
				throw new ArgumentException();

			if (stream is MemoryStream)
				return Deserialize((MemoryStream) stream, length);

			var buffer = new byte[length];
			stream.Read(buffer, 0, length);

			var tmpStream = new MemoryStream(buffer);
			return Deserialize(tmpStream, length);
		}

		public static Data Deserialize(MemoryStream stream, int length)
		{
			try
			{
				using (XmlReader reader = XmlReader.Create(stream))
				{
					var serializer = new DataContractSerializer(typeof(Packet), KnownTypes);
					object result = serializer.ReadObject(reader);
					return ((Packet)result).Data;
				}
			}
			catch (SerializationException e)
			{
				throw new ReadRequestException(e);
			}
			catch (XmlException e)
			{
				throw new ReadRequestException(e);
			}
		}
	}
}